import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { EventRow } from "@/components/event-row"; import { FieldChanges } from "@/components/field-changes"; import { LiveFeed } from "@/components/live-feed"; import { Badge, Bar, Chip, Empty, ExtLink, Flag, Heatmap, PageHeader, Panel, Score, Stat, Table, Tabs, Td, TierBadge, TypeChip } from "@/components/ui"; import { WatchButton } from "@/components/watch-button"; import { api, SITE_URL, type EntityDetail, type EntityInsights, type EventItem } from "@/lib/api"; import { agoIso, dayHeader, fmtInt, fmtScore, relTime, typeLabel, utcDate, utcDateTime, utcTime, withinLast } from "@/lib/format"; export const dynamic = "force-dynamic"; type Tab = "overview" | "live" | "sources" | "silent" | "timeline" | "related" | "metrics"; const TABS: Tab[] = ["overview", "live", "sources", "silent", "timeline", "related", "metrics"]; const RANGES: { key: string; label: string; ms: number | null }[] = [ { key: "1h", label: "1 h", ms: 3600e3 }, { key: "6h", label: "6 h", ms: 6 * 3600e3 }, { key: "24h", label: "24 h", ms: 24 * 3600e3 }, { key: "7d", label: "7 d", ms: 7 * 86400e3 }, { key: "30d", label: "30 d", ms: 30 * 86400e3 }, { key: "all", label: "All", ms: null }, ]; type Search = { tab?: string; range?: string; cursor?: string }; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; const d = await api.entity(id); if (!d) return { title: "Entity not found" }; const e = d.entity; const desc = e.description ?? `Every meaningful change WebSensor detected for ${e.name}: announcements, releases, pricing, documentation, incidents and silent changes — with evidence.`; return { title: `${e.name} — ${typeLabel(e.type)}`, description: desc, alternates: { canonical: `/entity/${e.id}` }, openGraph: { title: `${e.name} · WebSensor`, description: desc, url: `${SITE_URL}/entity/${e.id}` } }; } const EMPTY_INSIGHTS: EntityInsights = { heatmap: [], baseline_per_day: 0, today: 0, events_24h: 0, events_prev_24h: 0, velocity_ratio: 0, anomaly: { score: 0, ratio: 0, pct: 0 }, silent_24h: 0, breaking_24h: 0, sources_24h: 0, most_active_sensors: [], rank: { rank: null, total: 0, score: null } }; function mainCountry(d: EntityDetail): string | null { const counts = new Map(); for (const s of d.sources) if (s.country) counts.set(s.country, (counts.get(s.country) ?? 0) + (s.first_party ? 2 : 1)); let best: string | null = null; let n = 0; for (const [c, k] of counts) if (k > n) [best, n] = [c, k]; return best; } export default async function EntityPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise }) { const { id } = await params; const sp = await searchParams; const d = await api.entity(id); if (!d) notFound(); const e = d.entity; const tab: Tab = TABS.includes(sp.tab as Tab) ? (sp.tab as Tab) : "overview"; const ins = d.insights ?? EMPTY_INSIGHTS; const silent = d.silent ?? []; const related = d.related ?? []; const country = mainCountry(d); const active = withinLast(e.last_event_at, 7 * 86400e3); const anomalous = ins.anomaly.score >= 60; const delta = ins.events_24h - ins.events_prev_24h; const href = (t: Tab): string => (t === "overview" ? `/entity/${e.id}` : `/entity/${e.id}?tab=${t}`); return ( <> {d.parent && ( ↑ {d.parent.name} )} {e.domain && {e.domain}} {country && } {active ? "ACTIVE" : "QUIET"} {ins.rank.rank !== null && ( WebSensor rank #{fmtInt(ins.rank.rank)} of {fmtInt(ins.rank.total)} )} } title={e.name} description={e.description} actions={ <> Add to alert {(e.homepage || e.domain) && Website ↗} } />
0 ? "text-signal" : delta < 0 ? "text-fg-subtle" : ""}> {delta > 0 ? "↑" : delta < 0 ? "↓" : "→"} {Math.abs(delta)} vs prev 24 h · ×{ins.velocity_ratio.toFixed(2)} } />
0 ? "hot" : undefined} hint="signal ≥ 80" />
0 ? "silent" : undefined} hint="no matching announcement" />
{ins.baseline_per_day.toFixed(1)}/day} hint={`today ${fmtInt(ins.today)}`} />
0 ? fmtScore(ins.anomaly.score) : "0"} tone={anomalous ? "hot" : ins.anomaly.score >= 40 ? "warn" : undefined} hint={{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% vs baseline} />
35-day activity
less more ≥ 3 breaking
Silent, href: href("silent"), count: silent.length }, { key: "timeline", label: "Timeline", href: href("timeline") }, { key: "related", label: "Related", href: href("related"), count: related.length + d.children.length }, { key: "metrics", label: "Metrics", href: href("metrics") }, ]} /> {tab === "overview" && } {tab === "live" && } {tab === "sources" && } {tab === "silent" && } {tab === "timeline" && } {tab === "related" && } {tab === "metrics" && } ); } // --------------------------------------------------------------------------------------- function Overview({ d, ins }: { d: EntityDetail; ins: EntityInsights }) { const e = d.entity; const related = d.related ?? []; return (
Recent signals {d.recent.length}} dense action={full timeline →}> {d.recent.length ? d.recent.map((ev) => ) : No event detected yet for {e.name}. Sensors linked to this entity are checked continuously.}
); } function SensorsPanel({ sensors, title = "Most active sensors · 7 d" }: { sensors: EntityInsights["most_active_sensors"]; title?: string }) { return ( {sensors.length ? (
    {sensors.slice(0, 8).map((s) => (
  • {s.name}
    {s.source_id} · {relTime(s.last_event_at)}
    {s.type} {s.events_7d}
  • ))}
) : ( No sensor produced an event for this entity in the last 7 days. )}
); } function SourcesTab({ d }: { d: EntityDetail }) { return ( Monitored sources {d.sources.length}} dense> {d.sources.length === 0 ? ( No monitored source is linked to {d.entity.name} yet. Events still resolve to it from third-party coverage. ) : ( {[...d.sources].sort((a, b) => (b.events_24h ?? 0) - (a.events_24h ?? 0) || Number(Boolean(b.first_party)) - Number(Boolean(a.first_party))).map((s) => ( ))}
{s.name}
{s.domain}
{s.first_party ? : } {s.country ? {s.country} : —} {s.sensor_count ?? 0} {s.events_24h ?? 0} sensors →
)}
); } function SilentTab({ items, name }: { items: EventItem[]; name: string }) { return ( Silent changes {items.length}} dense action={all silent →}> {items.length === 0 ? ( No silent changes detected in this period. ) : (
    {items.map((ev) => (
  • {utcDate(ev.detected_at)}
    {ev.source?.name ?? ev.source_id} {ev.country && } {typeLabel(ev.event_type)} {ev.change_class && {ev.change_class}}
    {ev.title} {ev.field_changes && ev.field_changes.length > 0 ? (
    ) : ( ev.summary &&

    {ev.summary}

    )}
  • ))}
)}
); } async function TimelineTab({ id, range, cursor }: { id: string; range?: string; cursor?: string }) { const r = RANGES.find((x) => x.key === range) ?? RANGES[2]!; const after = r.ms ? agoIso(r.ms) : undefined; const page = await api.events({ entity: id, after, limit: 100, cursor }); const groups = new Map(); for (const ev of page.items) { const day = utcDate(ev.detected_at); if (!groups.has(day)) groups.set(day, []); groups.get(day)!.push(ev); } const rangeHref = (k: string): string => `/entity/${id}?tab=timeline&range=${k}`; return ( Timeline {page.items.length}{page.nextCursor ? "+" : ""}} dense action={ } > {page.items.length === 0 ? ( No event in the last {r.label.replace(" ", "")}{r.ms ? "" : " — nothing recorded yet"}. Try a wider range. ) : (
{[...groups.entries()].map(([day, evs]) => (
{dayHeader(evs[0]!.detected_at)} {evs.length}
{evs.map((ev) => )}
))}
)}
{after ? <>since {utcDateTime(after)} : "complete history, newest first"} {page.nextCursor && Older →}
); } function RelatedTab({ d }: { d: EntityDetail }) { const related = d.related ?? []; const max = Math.max(1, ...related.map((r) => r.shared_events)); return (
Co-occurring entities {related.length}} dense> {related.length === 0 ? ( No entity shares an event with {d.entity.name} yet. ) : ( {related.map((r) => ( ))}
{r.name} {typeLabel(r.type)} {r.shared_events}
)}
Products & children {d.children.length}} dense> {d.children.length === 0 ? ( No child entity. ) : ( {d.children.map((c) => ( ))}
{c.name} {typeLabel(c.type)} {fmtInt(c.event_count)} {c.last_event_at ? relTime(c.last_event_at) : "—"}
)}
Knowledge graph {d.relations.length}} dense> {d.relations.length === 0 ? ( No relation recorded. ) : (
    {d.relations.map((r, i) => (
  • {r.from_name} {r.relation.replace(/_/g, " ")} {r.to_name} {typeLabel(r.to_type)}
  • ))}
)}
{d.aliases.length > 0 && (
{d.aliases.map((a) => {a})}
)}
); } function MetricsTab({ d, ins }: { d: EntityDetail; ins: EntityInsights }) { const maxType = Math.max(1, ...d.by_type.map((t) => t.n)); const total = d.by_type.reduce((n, t) => n + t.n, 0); const anomalous = ins.anomaly.score >= 60; const days7 = ins.heatmap.slice(-7).reduce((n, x) => n + x.events, 0); const days35 = ins.heatmap.reduce((n, x) => n + x.events, 0); const peak = ins.heatmap.reduce<{ day: string; events: number } | null>((best, x) => (!best || x.events > best.events ? { day: x.day, events: x.events } : best), null); return (
Event types {fmtInt(total)}}> {d.by_type.length ? (
    {d.by_type.map((t) => (
  • {typeLabel(t.event_type)} {t.n} {total ? Math.round((t.n / total) * 100) : 0}%
  • ))}
) : ( No events yet. )}
= 40 ? "text-warn" : "text-fg"}`}>{ins.anomaly.score > 0 ? fmtScore(ins.anomaly.score) : "0"} {anomalous ? "ANOMALOUS ACTIVITY" : ins.anomaly.score >= 40 ? "ELEVATED" : "NORMAL"}
= 40 ? "high" : "signal"} />
30-day baseline
{ins.baseline_per_day.toFixed(1)} events/day
Last 24 h
{fmtInt(ins.events_24h)} events
Previous 24 h
{fmtInt(ins.events_prev_24h)} events
Velocity ratio
×{ins.velocity_ratio.toFixed(2)}
vs baseline
{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% (×{ins.anomaly.ratio.toFixed(2)})

The anomaly score compares the last 24 h with this entity's own 30-day baseline, so a busy organization is not flagged for being busy. Scores ≥ 60 mark anomalous activity; the ratio above 1 means more events than usual.

7 d
{fmtInt(days7)}
35 d
{fmtInt(days35)}
Peak day
{peak && peak.events ? <>{peak.events} {peak.day} : "—"}
); }